Jakub’s hardloop data

Python
Machine Learning
Een netwerk voor hardloopprestaties.
Author

Sobotka

Published

June 25, 2026

Code
import pandas as pd

activities = client.get_activities(0, 582)

df_raw = pd.DataFrame(activities)

relevante_kolommen = [
    'startTimeLocal', 
    'distance', 
    'duration', 
    'averageHR', 
    'maxHR' 
]

bestaande_kolommen = [col for col in relevante_kolommen if col in df_raw.columns]

df_raw[bestaande_kolommen].head()
startTimeLocal distance duration averageHR maxHR
0 2026-06-25 06:52:30 10138.830078 3435.436035 135.0 158.0
1 2026-06-24 17:04:00 57036.050781 6636.770020 160.0 185.0
2 2026-06-24 07:26:44 15132.349609 4892.004883 146.0 172.0
3 2026-06-22 17:45:54 26993.650391 3621.467041 141.0 162.0
4 2026-06-22 09:28:04 0.000000 2268.650879 112.0 165.0
Code
df_raw['afstand_km'] = df_raw['distance'] / 1000
df_raw['duur_min'] = df_raw['duration'] / 60
Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime

if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")

df_recent = df_raw[df_raw['datum'] >= start_van_het_jaar].copy()

df_week = df_recent.groupby(pd.Grouper(key='datum', freq='W-MON'))['afstand_km'].sum().reset_index()

# 4. De grafiek tekenen
plt.figure(figsize=(12, 6))
plt.bar(df_week['datum'], df_week['afstand_km'], width=5, color='#2c3e50', label='Afgelegde kilometers')

plt.title(f'Wekelijkse Kilometers ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Totale Afstand (km)', fontsize=12)
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.5)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime

if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")

toegestane_sporten = ['running', 'trail_running']

df_recent = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten))
].copy()

df_week = df_recent.groupby(pd.Grouper(key='datum', freq='W-MON'))['afstand_km'].sum().reset_index()

plt.figure(figsize=(12, 6))
plt.bar(df_week['datum'], df_week['afstand_km'], width=5, color='#2c3e50', label='Hardloop & Trail km')

plt.title(f'Wekelijkse Hardloop Kilometers ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Totale Afstand (km)', fontsize=12)
plt.legend()
plt.grid(axis='y', linestyle='--', alpha=0.5)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime

if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000
if 'duur_min' not in df_raw.columns:
    df_raw['duur_min'] = df_raw['duration'] / 60

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")
toegestane_sporten = ['running', 'trail_running']

df_pace = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten)) &
    (df_raw['afstand_km'] > 0)
].copy()


df_pace['tempo_decimaal'] = df_pace['duur_min'] / df_pace['afstand_km']

df_pace = df_pace.sort_values('datum')

df_pace['trendlijn'] = df_pace['tempo_decimaal'].rolling(window=5, min_periods=1).mean()

plt.figure(figsize=(12, 6))

plt.scatter(df_pace['datum'], df_pace['tempo_decimaal'], color='#3498db', alpha=0.6, label='Individuele Run')

plt.plot(df_pace['datum'], df_pace['trendlijn'], color='#e74c3c', linewidth=2, label='Trend (Rollend gem. 5 runs)')

plt.title(f'Tempo per Run ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Tempo (min/km, decimaal)', fontsize=12)

plt.gca().invert_yaxis()

plt.legend()
plt.grid(axis='both', linestyle='--', alpha=0.4)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime


if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))


huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")
toegestane_sporten = ['running', 'trail_running']

df_hartslag = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten)) &
    (df_raw['averageHR'] > 0) 
].copy()


df_hartslag = df_hartslag.sort_values('datum')



df_hartslag['trendlijn'] = df_hartslag['averageHR'].rolling(window=5, min_periods=1).mean()


plt.figure(figsize=(12, 6))

plt.scatter(df_hartslag['datum'], df_hartslag['averageHR'], color='#9b59b6', alpha=0.5, label='Gem. Hartslag per Run')

plt.plot(df_hartslag['datum'], df_hartslag['trendlijn'], color='#8e44ad', linewidth=2.5, label='Trend (5 runs)')


plt.title(f'Gemiddelde Hartslag per Run ({huidig_jaar})', fontsize=16, fontweight='bold')
plt.xlabel('Datum', fontsize=12)
plt.ylabel('Hartslag (bpm)', fontsize=12)
plt.legend()

plt.grid(axis='y', linestyle='--', alpha=0.6)

plt.show()

Code
import matplotlib.pyplot as plt
import pandas as pd
import datetime


if 'datum' not in df_raw.columns:
    df_raw['datum'] = pd.to_datetime(df_raw['startTimeLocal'], utc=True).dt.tz_localize(None)
else:
    df_raw['datum'] = pd.to_datetime(df_raw['datum'], utc=True).dt.tz_localize(None)

if 'afstand_km' not in df_raw.columns:
    df_raw['afstand_km'] = df_raw['distance'] / 1000
if 'duur_min' not in df_raw.columns:
    df_raw['duur_min'] = df_raw['duration'] / 60

if 'sport' not in df_raw.columns and 'activityType' in df_raw.columns:
    df_raw['sport'] = df_raw['activityType'].apply(lambda x: x.get('typeKey', '') if isinstance(x, dict) else str(x))

huidig_jaar = datetime.datetime.now().year
start_van_het_jaar = pd.to_datetime(f"{huidig_jaar}-01-01")
toegestane_sporten = ['running', 'trail_running']

df_combo = df_raw[
    (df_raw['datum'] >= start_van_het_jaar) & 
    (df_raw['sport'].isin(toegestane_sporten)) &
    (df_raw['averageHR'] > 0) &
    (df_raw['afstand_km'] > 0)
].copy()

df_combo['tempo_decimaal'] = df_combo['duur_min'] / df_combo['afstand_km']
df_combo = df_combo.sort_values('datum')

df_combo['trend_hr'] = df_combo['averageHR'].rolling(window=5, min_periods=1).mean()
df_combo['trend_tempo'] = df_combo['tempo_decimaal'].rolling(window=5, min_periods=1).mean()

fig, ax1 = plt.subplots(figsize=(12, 6))
#laag 1
kleur_hr = '#e74c3c'  
ax1.scatter(df_combo['datum'], df_combo['averageHR'], color=kleur_hr, alpha=0.2)
ax1.plot(df_combo['datum'], df_combo['trend_hr'], color=kleur_hr, linewidth=3, label='Trend Hartslag (bpm)')
ax1.set_xlabel('Datum', fontsize=12)
ax1.set_ylabel('Hartslag (bpm)', color=kleur_hr, fontsize=12, fontweight='bold')
ax1.tick_params(axis='y', labelcolor=kleur_hr)

#laag 2
ax2 = ax1.twinx()  
kleur_tempo = '#3498db'  
ax2.scatter(df_combo['datum'], df_combo['tempo_decimaal'], color=kleur_tempo, alpha=0.2)
ax2.plot(df_combo['datum'], df_combo['trend_tempo'], color=kleur_tempo, linewidth=3, label='Trend Tempo (min/km)')
ax2.set_ylabel('Tempo (min/km)', color=kleur_tempo, fontsize=12, fontweight='bold')
ax2.tick_params(axis='y', labelcolor=kleur_tempo)

ax2.invert_yaxis()

plt.title(f'Hartslag vs Tempo ', fontsize=16, fontweight='bold')
ax1.grid(axis='x', linestyle='--', alpha=0.4)

fig.legend(loc='upper right', bbox_to_anchor=(0.9, 0.88))

plt.show()

Code
import folium
import pandas as pd
from IPython.display import display

run_data = df_recent.copy()
totaal_runs = len(run_data)

print(f"🚀 Start proces... Er staan {totaal_runs} runs in de wachtrij.")

if totaal_runs == 0:
    print(" Let op: df_recent is leeg. Ga even terug naar de cel waar je df_recent aanmaakt en draai die opnieuw.")
else:
    heatmap = folium.Map(location=[52.46, 4.62], zoom_start=11, tiles='CartoDB dark_matter')

    succesvol = 0
    overgeslagen = 0

    for index, run in run_data.iterrows():
        activity_id = run['activityId']
        
        try:
            details = client.get_activity_details(activity_id)
            
            if details and details.get('geoPolylineDTO') and details['geoPolylineDTO'].get('polyline'):
                gps_punten = details['geoPolylineDTO']['polyline']
                
                route = [
                    [punt['lat'], punt['lon']] 
                    for punt in gps_punten 
                    if punt.get('lat') is not None and punt.get('lon') is not None
                ]
                
                if route:
                    folium.PolyLine(
                        locations=route,
                        color='#fc4c02',
                        weight=2,        
                        opacity=0.3      
                    ).add_to(heatmap)
                    succesvol += 1
            else:
                overgeslagen += 1
                
        except Exception as e:
            # Als Garmin de verbinding weigert
            print(f"Fout bij ophalen run {activity_id}: {e}")
            overgeslagen += 1
            
    heatmap.save("mijn_persoonlijke_heatmap.html")

    display(heatmap)
🚀 Start proces... Er staan 97 runs in de wachtrij.
Make this Notebook Trusted to load map: File -> Trust Notebook
Code
import pandas as pd
import plotly.express as px
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler


df_ml = df_combo.dropna(subset=['afstand_km', 'tempo_decimaal', 'averageHR']).copy()

features = ['afstand_km', 'tempo_decimaal', 'averageHR']
X = df_ml[features]

scaler = StandardScaler()
X_geschaald = scaler.fit_transform(X)

aantal_clusters = 3

kmeans = KMeans(n_clusters=aantal_clusters, random_state=42, n_init=10)
df_ml['Cluster'] = kmeans.fit_predict(X_geschaald)

df_ml['Cluster'] = 'Cluster ' + df_ml['Cluster'].astype(str)

fig = px.scatter_3d(
    df_ml, 
    x='afstand_km', 
    y='tempo_decimaal', 
    z='averageHR',
    color='Cluster',
    title=f'K-Means Clustering ({aantal_clusters} Trainingsprofielen)',
    labels={
        'afstand_km': 'Afstand (km)',
        'tempo_decimaal': 'Tempo (min/km)',
        'averageHR': 'Gem. Hartslag (bpm)'
    },
    opacity=0.8
)

fig.update_layout(scene=dict(yaxis=dict(autorange='reversed')))

cluster_profielen = df_ml.groupby('Cluster')[['afstand_km', 'tempo_decimaal', 'averageHR']].mean()

cluster_profielen = cluster_profielen.round(2)

cluster_profielen = cluster_profielen.sort_values('afstand_km')

print(cluster_profielen)


fig.show()
           afstand_km  tempo_decimaal  averageHR
Cluster                                         
Cluster 1       10.13            5.64     140.66
Cluster 0       11.33            4.68     168.80
Cluster 2       24.40            5.69     149.14

Code
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

df_ml = df_ml.sort_values('datum')


df_ml['Volgende_Training'] = df_ml['Cluster'].shift(-1)


df_markov = df_ml.dropna(subset=['Volgende_Training'])


transitie_matrix = pd.crosstab(
    df_markov['Cluster'], 
    df_markov['Volgende_Training'], 
    normalize='index'
)

transitie_matrix = (transitie_matrix * 100).round(1)

plt.figure(figsize=(10, 6))
sns.heatmap(transitie_matrix, annot=True, fmt='g', cmap='Blues', cbar_kws={'label': 'Kans (%)'})

plt.title('Wat is de kans op de volgende training?', fontsize=14, fontweight='bold')
plt.xlabel('Voorspelde VOLGENDE Training', fontsize=12)
plt.ylabel('Jouw HUIDIGE Training', fontsize=12)

plt.show()

laatste_run = df_ml.iloc[-1]['Cluster']
voorspelling = transitie_matrix.loc[laatste_run].idxmax()
kans = transitie_matrix.loc[laatste_run].max()

print("\n--- JOUW AI VOORSPELLING ---")
print(f"Je laatste run was een: '{laatste_run}'.")
print(f"Op basis van jouw eigen data is de kans het grootst ({kans}%) dat je volgende run een '{voorspelling}' wordt.")


--- JOUW AI VOORSPELLING ---
Je laatste run was een: 'Cluster 1'.
Op basis van jouw eigen data is de kans het grootst (51.9%) dat je volgende run een 'Cluster 1' wordt.
Code
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

df_afstand = df_combo.copy()
df_afstand = df_afstand.sort_values('datum')


bins = [0, 5, 10, 15, 20, 25, 30, np.inf]

labels = ['0-5 km', '5-10 km', '10-15 km', '15-20 km', '20-25 km', '25-30 km', '30+ km']

df_afstand['Afstand_Categorie'] = pd.cut(df_afstand['afstand_km'], bins=bins, labels=labels)

df_afstand['Volgende_Afstand'] = df_afstand['Afstand_Categorie'].shift(-1)

df_afstand = df_afstand.dropna(subset=['Volgende_Afstand'])

transitie_matrix_km = pd.crosstab(
    df_afstand['Afstand_Categorie'], 
    df_afstand['Volgende_Afstand'], 
    normalize='index' 
)

transitie_matrix_km = (transitie_matrix_km * 100).round(1)

plt.figure(figsize=(12, 8))

sns.heatmap(transitie_matrix_km, annot=True, fmt='g', cmap='YlGnBu', cbar_kws={'label': 'Kans (%)'})

plt.title('Wat is de kans op afstand X na Y?', fontsize=16, fontweight='bold')
plt.xlabel('Voorspelde VOLGENDE Afstand', fontsize=12)
plt.ylabel('Jouw HUIDIGE Afstand', fontsize=12)

plt.xticks(rotation=45)
plt.yticks(rotation=0)

plt.tight_layout()
plt.show()

laatste_afstand_km = df_afstand.iloc[-1]['afstand_km']
laatste_bakje = df_afstand.iloc[-1]['Afstand_Categorie']
voorspeld_bakje = transitie_matrix_km.loc[laatste_bakje].idxmax()
kans = transitie_matrix_km.loc[laatste_bakje].max()

print("\n--- KILOMETER VOORSPELLING ---")
print(f"Je laatste run was exact {laatste_afstand_km:.1f} km (Categorie: '{laatste_bakje}').")
print(f"Historisch gezien is de kans het grootst ({kans}%) dat je volgende run tussen de '{voorspeld_bakje}' wordt.")


--- KILOMETER VOORSPELLING ---
Je laatste run was exact 15.1 km (Categorie: '15-20 km').
Historisch gezien is de kans het grootst (58.3%) dat je volgende run tussen de '10-15 km' wordt.
Code
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import StandardScaler
import numpy as np

# We pakken de tabel die we eerder hebben schoongemaakt
df_nn = df_combo.dropna(subset=['afstand_km', 'duur_min', 'averageHR']).copy()

# We voegen je gewicht toe als feature voor de energie-berekening
df_nn['gewicht_kg'] = 61.0 

# X_train (De Input Matrix): Afstand, Hartslag en Gewicht
X_train = df_nn[['afstand_km', 'averageHR', 'gewicht_kg']].values

# y_train (De Output Vector): Daadwerkelijke tijd in minuten
y_train = df_nn['duur_min'].values

# Normaliseer de input (extreem belangrijk voor Neurale Netwerken)
scaler_X = StandardScaler()
X_train_scaled = scaler_X.fit_transform(X_train)

# Zet alles om naar wiskundige PyTorch Tensors
X_tensor = torch.tensor(X_train_scaled, dtype=torch.float32)
y_tensor = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)


# --- 2. DE MOTOR: HET NEURALE NETWERK ---
class FinishtijdVoorspeller(nn.Module):
    def __init__(self, input_dim):
        super(FinishtijdVoorspeller, self).__init__()
        self.layer1 = nn.Linear(input_dim, 16)
        self.act1 = nn.ReLU()
        
        self.layer2 = nn.Linear(16, 8)
        self.act2 = nn.ReLU()
        
        self.output_layer = nn.Linear(8, 1) # 1 output: de tijd in minuten
        
    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.act2(self.layer2(x))
        x = self.output_layer(x)
        return x

# Er zijn 3 input variabelen (afstand, hartslag, gewicht)
model = FinishtijdVoorspeller(input_dim=3)


# --- 3. TRAINING: BACKPROPAGATION ---
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.05)

print("Start met het trainen van het Neurale Netwerk...\n")

# We trainen het model in 500 stapjes (epochs)
for epoch in range(500):
    model.train()
    
    # Forward pass
    voorspellingen = model(X_tensor)
    loss = criterion(voorspellingen, y_tensor)
    
    # Backward pass (Gradiënten updaten)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
Start met het trainen van het Neurale Netwerk...
Code
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import StandardScaler
import pandas as pd
import numpy as np


print("Data aan het voorbereiden...")

df_nn = df_combo.copy()
df_nn = df_nn.sort_values('datum')

if 'elevationGain' in df_nn.columns:
    df_nn['hoogtemeters'] = df_nn['elevationGain'].fillna(0)
else:
    df_nn['hoogtemeters'] = 0.0


df_nn = df_nn.set_index('datum')

df_nn['volume_28d'] = df_nn['afstand_km'].rolling('28D').sum().shift(1).fillna(0)
df_nn = df_nn.reset_index()

df_nn['snelheid_kmu'] = 60 / df_nn['tempo_decimaal']
df_nn['efficientie'] = df_nn['snelheid_kmu'] / df_nn['averageHR']

df_nn['gewicht'] = 61.0

features = ['afstand_km', 'hoogtemeters', 'volume_28d', 'efficientie', 'gewicht']
df_nn = df_nn.dropna(subset=features + ['duur_min'])

X_train = df_nn[features].values
y_train = df_nn['duur_min'].values

scaler_X = StandardScaler()
X_train_scaled = scaler_X.fit_transform(X_train)

X_tensor = torch.tensor(X_train_scaled, dtype=torch.float32)
y_tensor = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)



class FinishtijdVoorspeller(nn.Module):
    def __init__(self, input_dim):
        super(FinishtijdVoorspeller, self).__init__()
        self.layer1 = nn.Linear(input_dim, 16)
        self.act1 = nn.ReLU()
        self.layer2 = nn.Linear(16, 8)
        self.act2 = nn.ReLU()
        self.output_layer = nn.Linear(8, 1)
        
    def forward(self, x):
        x = self.act1(self.layer1(x))
        x = self.act2(self.layer2(x))
        x = self.output_layer(x)
        return x

model = FinishtijdVoorspeller(input_dim=5)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.05)

print("Netwerk architectuur staat klaar. Start training van 500 epochs...\n")

for epoch in range(500):
    model.train()
    optimizer.zero_grad()
    
    voorspellingen = model(X_tensor)
    loss = criterion(voorspellingen, y_tensor)
    
    loss.backward()
    optimizer.step()
    
    if (epoch + 1) % 100 == 0:
        print(f"Epoch {epoch+1}/500 - Loss (Foutmarge): {loss.item():.2f}")


print("\n--- AI TEST ---")
test_afstand = 172.0    
test_hoogtemeters = 5200 
test_volume = 312      
test_eff = df_nn['efficientie'].mean() 
test_gewicht = 61.0

nieuwe_race = np.array([[test_afstand, test_hoogtemeters, test_volume, test_eff, test_gewicht]])
nieuwe_race_scaled = scaler_X.transform(nieuwe_race)
nieuwe_race_tensor = torch.tensor(nieuwe_race_scaled, dtype=torch.float32)

model.eval()
with torch.no_grad():
    voorspelde_minuten = model(nieuwe_race_tensor).item()

uur = int(voorspelde_minuten // 60)
minuten = int(voorspelde_minuten % 60)
print(f"Voor duurloop van {test_afstand}km schat de AI jouw tijd op: {uur} uur en {minuten:02d} minuten.")
Data aan het voorbereiden...
Netwerk architectuur staat klaar. Start training van 500 epochs...

Epoch 100/500 - Loss (Foutmarge): 36.00
Epoch 200/500 - Loss (Foutmarge): 28.04
Epoch 300/500 - Loss (Foutmarge): 26.58
Epoch 400/500 - Loss (Foutmarge): 24.54
Epoch 500/500 - Loss (Foutmarge): 22.53

--- AI TEST ---
Voor duurloop van 172.0km schat de AI jouw tijd op: 19 uur en 01 minuten.